#include <graphics/Camera.h>

namespace ne
{
    Camera::Camera()
        : Node(),
          mFOV(Math::QUARTER_PI),
          mRatio(16.0f / 9.0f), // 16 : 9
          mFocalLength(1.0f),
          mNear(1.0f),
          mFar(1000.0f),
          mProjectionType(PT_PERSPECTIVE),
          mCullFrustum(),
          mFrustumOffset(Vector2::ZERO),
          mOrthoHeight(720.0f),
          mProjectionMatrix(Matrix4::ZERO),
          mDirtyProjection(true),
          mDirtyFrustumPlanes(true)
    {
    }

    void Camera::setScale(const Vector3 &v)
    {
    }

    void Camera::setFOV(const real a)
    {
        mFOV = a;
        _invalidateProjection();
    }

    real Camera::getFOV() const
    {
        return mFOV;
    }

    void Camera::setAspectRatio(const real a)
    {
        mRatio = a;
        _invalidateProjection();
    }

    real Camera::getAspectRatio() const
    {
        return mRatio;
    }

    void Camera::setFocalLength(const real a)
    {
        mFocalLength = a;
        _invalidateProjection();
    }

    real Camera::getFocalLength() const
    {
        return mFocalLength;
    }

    void Camera::setNearClipDistance(const real a)
    {
        mNear = a;
        _invalidateProjection();
    }

    real Camera::getNearClipDistance() const
    {
        return mNear;
    }

    void Camera::setFarClipDistance(const real a)
    {
        mFar = a;
        _invalidateProjection();
    }

    real Camera::getFarClipDistance() const
    {
        return mFar;
    }

    const Matrix4& Camera::getViewMatrix() const
    {
        return getInverseTransformMatrix();
    }

    const Matrix4& Camera::getProjectionMatrix() const
    {
        _updateProjection();
        return mProjectionMatrix;
    }

    void Camera::setProjectionType(const ProjectionType pt)
    {
        mProjectionType = pt;
        _invalidateProjection();
    }

    ProjectionType Camera::getProjectionType() const
    {
        return mProjectionType;
    }

    const Frustum& Camera::getCullingFrustum() const
    {
        _updateFrustumPlanes();
        return mCullFrustum;
    }

    void Camera::setFrustumOffset(const Vector2 &v)
    {
        mFrustumOffset = v;
        _invalidateProjection();
    }

    const Vector2& Camera::getFrustumOffset() const
    {
        return mFrustumOffset;
    }

    void Camera::setOrthoWindowWidth(const real a)
    {
        mOrthoHeight = a / mRatio;
        _invalidateProjection();
    }

    real Camera::getOrthoWindowWidth() const
    {
        return mOrthoHeight * mRatio;
    }

    void Camera::setOrthoWindowHeight(const real a)
    {
        mOrthoHeight = a;
        _invalidateProjection();
    }

    real Camera::getOrthoWindowHeight() const
    {
        return mOrthoHeight;
    }

    Ray Camera::getViewportToWorldRay(const real x, const real y) const
    {
        Ray resRay;
        getViewportToWorldRay(x, y, resRay);
        return resRay;
    }

    void Camera::getViewportToWorldRay(const real x, const real y, Ray &outRay) const
    {
        Matrix4 inverseVP = (getViewMatrix() * getProjectionMatrix()).inverse();

        // Relative the viewport
        Vector3 nearPoint(x, y, -1.0f);
        Vector3 midPoint(x, y, 0.0f);

        // Get ray origin and ray target on near plane in world space
        Vector3 rayTarget = midPoint * inverseVP;
        outRay.origin = nearPoint * inverseVP;
        outRay.direction = rayTarget - outRay.origin;
        outRay.normalize();
    }

    Ray Camera::getViewportToCameraRay(const real x, const real y) const
    {
        Ray resRay;
        getViewportToCameraRay(x, y, resRay);
        return resRay;
    }

    void Camera::getViewportToCameraRay(const real x, const real y, Ray &outRay) const
    {
        Matrix4 inverseP = getProjectionMatrix().inverse();

        // Relative the viewport
        Vector3 nearPoint(x, y, -1.0f);
        Vector3 midPoint(x, y, 0.0f);

        // Get ray origin and ray target on near plane in world space
        Vector3 rayTarget = midPoint * inverseP;
        outRay.origin = nearPoint * inverseP;
        outRay.direction = rayTarget - outRay.origin;
        outRay.normalize();
    }

    void Camera::needUpdate(const bool forceParentUpdate)
    {
        Node::needUpdate(forceParentUpdate);
        _invalidateFrustumPlanes();
    }

    Camera::~Camera()
    {
    }

    void Camera::_invalidateProjection() const
    {
        mDirtyProjection = true;
        _invalidateFrustumPlanes();
    }

    void Camera::_invalidateFrustumPlanes() const
    {
        mDirtyFrustumPlanes = true;
    }

    void Camera::_updateProjection() const
    {
        if (mDirtyProjection)
        {
            real left, right, top, bottom;
            _calcProjectionParameters(left, right, top, bottom);
            real invW = 1.0f / (right - left);
            real invH = 1.0f / (top - bottom);
            real invD = 1.0f / (mFar - mNear);

            if (mProjectionType == PT_PERSPECTIVE)
            {
                real A = 2.0f * mNear * invW;
                real B = 2.0f * mNear * invH;
                real C = (right + left) * invW;
                real D = (top + bottom) * invH;
                real q, qn;
                if (mFar == 0.0f)
                {
                    // Infinite far plane
                    q = Math::EPSILON - 1.0f;
                    qn = mNear * (q - 1.0f);
                }
                else
                {
                    q = - (mFar + mNear) * invD;
                    qn = -2.0f * (mFar * mNear) * invD;
                }
                // [ A   0   0   0  ]
                // [ 0   B   0   0  ]
                // [ C   D   q  -1  ]
                // [ 0   0   qn  0  ]
                mProjectionMatrix = Matrix4::ZERO;
                mProjectionMatrix[0][0] = A;
                mProjectionMatrix[1][1] = B;
                mProjectionMatrix[2][0] = C;
                mProjectionMatrix[2][1] = D;
                mProjectionMatrix[2][2] = q;
                mProjectionMatrix[3][2] = qn;
                mProjectionMatrix[2][3] = -1.0f;
            }
            else if (mProjectionType == PT_ORTHOGRAPHIC)
            {
                real A = 2.0f * invW;
                real B = 2.0f * invH;
                real C = -(right + left) * invW;
                real D = -(top + bottom) * invH;
                real q, qn;
                if (mFar == 0.0f)
                {
                    // Infinite far plane
                    q = -Math::EPSILON / mNear;
                    qn = -Math::EPSILON - 1.0f;
                }
                else
                {
                    q = - 2.0f * invD;
                    qn = -(mFar + mNear) * invD;
                }
                // [ A   0   0   0  ]
                // [ 0   B   0   0  ]
                // [ 0   0   q   0  ]
                // [ C   D   qn  1  ]
                mProjectionMatrix = Matrix4::IDENTITY;
                mProjectionMatrix[0][0] = A;
                mProjectionMatrix[1][1] = B;
                mProjectionMatrix[3][0] = C;
                mProjectionMatrix[3][1] = D;
                mProjectionMatrix[2][2] = q;
                mProjectionMatrix[3][2] = qn;
                mProjectionMatrix[3][3] = 1.0f;
            }
            mDirtyProjection = false;
        }
    }

    // TODO: Maybe something wrong, need to fix
    void Camera::_updateFrustumPlanes() const
    {
        if (mDirtyFrustumPlanes)
        {
            Matrix4 combo = getViewMatrix() * getProjectionMatrix();
            Plane *pPlane = 0;

            pPlane = mCullFrustum.getPlane(FRUSTUM_PLANE_NEAR);
            pPlane->redefine(combo[0][3] + combo[0][2],
                             combo[1][3] + combo[1][2],
                             combo[2][3] + combo[2][2],
                             combo[3][3] + combo[3][2]);

            pPlane = mCullFrustum.getPlane(FRUSTUM_PLANE_FAR);
            pPlane->redefine(combo[0][3] - combo[0][2],
                             combo[1][3] - combo[1][2],
                             combo[2][3] - combo[2][2],
                             combo[3][3] - combo[3][2]);

            pPlane = mCullFrustum.getPlane(FRUSTUM_PLANE_LEFT);
            pPlane->redefine(combo[0][3] + combo[0][0],
                             combo[1][3] + combo[1][0],
                             combo[2][3] + combo[2][0],
                             combo[3][3] + combo[3][0]);

            pPlane = mCullFrustum.getPlane(FRUSTUM_PLANE_RIGHT);
            pPlane->redefine(combo[0][3] - combo[0][0],
                             combo[1][3] - combo[1][0],
                             combo[2][3] - combo[2][0],
                             combo[3][3] - combo[3][0]);

            pPlane = mCullFrustum.getPlane(FRUSTUM_PLANE_TOP);
            pPlane->redefine(combo[0][3] - combo[0][1],
                             combo[1][3] - combo[1][1],
                             combo[2][3] - combo[2][1],
                             combo[3][3] - combo[3][1]);

            pPlane = mCullFrustum.getPlane(FRUSTUM_PLANE_BOTTON);
            pPlane->redefine(combo[0][3] + combo[0][1],
                             combo[1][3] + combo[1][1],
                             combo[2][3] + combo[2][1],
                             combo[3][3] + combo[3][1]);

            mDirtyFrustumPlanes = false;
        }
    }

    void Camera::_calcProjectionParameters(real &left, real &right, real &top, real &bottom) const
    {
        if (mProjectionType == PT_PERSPECTIVE)
        {
            real radianY = mFOV * 0.5f;
            real tanY = std::tan(radianY);
            real tanX = tanY * mRatio;

            real nearFocal = mNear / mFocalLength;
            Vector2 nearOffset = mFrustumOffset * nearFocal;
            real halfWidth = tanX * mNear;
            real halfHeight = tanY * mNear;

            left   = nearOffset.x - halfWidth;
            right  = nearOffset.x + halfWidth;
            top    = nearOffset.y + halfHeight;
            bottom = nearOffset.y - halfHeight;
        }
        else if (mProjectionType == PT_ORTHOGRAPHIC)
        {
            real halfWidth = getOrthoWindowWidth() * 0.5f;
            real halfHeight = getOrthoWindowHeight() * 0.5f;

            left   = - halfWidth;
            right  = + halfWidth;
            top    = + halfHeight;
            bottom = - halfHeight;
        }
    }
}
